package com.dyna.auth.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.dyna.api.domain.entity.SysOAuth2AccessToken;
import com.dyna.api.domain.entity.SysOAuth2RefreshToken;
import com.dyna.api.domain.entity.SystemOath2Client;
import com.dyna.api.domain.resp.AuthLoginResp;
import com.dyna.api.domain.resp.SysOAuth2AccessTokenCheckResp;
import com.dyna.auth.mapper.OAuth2AccessTokenMapper;
import com.dyna.auth.mapper.OAuth2RefreshTokenMapper;
import com.dyna.auth.service.Oath2ClientService;
import com.dyna.auth.service.TokenService;
import com.dyna.constants.ModuleErrorCodeConstants;
import com.dyna.constants.OAuth2ClientConstants;
import com.dyna.constants.RedisKeyConstant;
import com.dyna.core.exception.CustomException;
import com.dyna.core.utils.CollUtil;
import com.dyna.core.utils.DateUtil;
import com.dyna.core.utils.ObjectUtil;
import com.dyna.domain.dto.LoginUser;
import com.dyna.domain.resp.R;
import com.dyna.domain.resp.ResultCode;
import com.dyna.foundation.feign.FoundationUserApi;
import com.dyna.utils.RdsUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author zhoucaiwang
 * @date 2023/8/15
 */
@Slf4j
@Service
@AllArgsConstructor
public class TokenServiceImpl implements TokenService {

    private final Oath2ClientService oauth2ClientService;
    private final OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
    private final OAuth2AccessTokenMapper oAuth2AccessTokenMapper;
    @Qualifier("com.dyna.foundation.feign.FoundationUserApi")
    private final FoundationUserApi foundationUserApi;

    @Override
    public AuthLoginResp createAccessToken(Long userId, String clientIdDefault, List<String> scopes) {
//        LoginUser userCache = RdsUtil.get(RedisKeyConstant.USER_INFO_KEY + loginUser.getUsername(), LoginUser.class);
        SystemOath2Client clientDO = oauth2ClientService.validOAuthClientFromCache(OAuth2ClientConstants.CLIENT_ID_DEFAULT, null, null, null, null);
        // 创建刷新令牌
        SysOAuth2RefreshToken refreshTokenDO = createOAuth2RefreshToken(userId, clientDO, scopes);
        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO);
    }

    private AuthLoginResp createOAuth2AccessToken(SysOAuth2RefreshToken refreshTokenDO, SystemOath2Client clientDO) {
        LocalDateTime expiresTime = LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds());
        LoginUser loginUser = buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType());
        SysOAuth2AccessToken accessTokenDO = new SysOAuth2AccessToken().setAccessToken(IdUtil.fastSimpleUUID())
                .setUserId(refreshTokenDO.getUserId())
                .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes())
                .setUserInfo(loginUser)
                .setExpiresTime(expiresTime)
                .setRefreshToken(refreshTokenDO.getRefreshToken());
        oAuth2AccessTokenMapper.insert(accessTokenDO);
        // 记录到 Redis 中
        this.setCacheLogin(accessTokenDO);
        return BeanUtil.copyProperties(accessTokenDO, AuthLoginResp.class);
    }

    private void setCacheLogin(SysOAuth2AccessToken accessTokenDO) {
        String redisKey = formatKey(accessTokenDO.getAccessToken());
        // 清理多余字段，避免缓存
        long time = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS);
        if (time > 0) {
            RdsUtil.set(redisKey, JSON.toJSONString(accessTokenDO),time, TimeUnit.SECONDS);
        }
    }

    private static String formatKey(String accessToken) {
        return String.format(RedisKeyConstant.OAUTH2_ACCESS_TOKEN, accessToken);
    }

    /**
     * 创建访问刷新令牌
     * @param userId 用户id
     * @param clientDO 客户端
     * @param scopes 授权范围
     * @return SysOAuth2RefreshToken
     */
    private SysOAuth2RefreshToken createOAuth2RefreshToken(Long userId, SystemOath2Client clientDO, List<String> scopes) {
        SysOAuth2RefreshToken refreshToken = new SysOAuth2RefreshToken().setRefreshToken(IdUtil.fastSimpleUUID())
                .setUserId(userId)
                .setClientId(clientDO.getClientId()).setScopes(scopes)
                .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
        oauth2RefreshTokenMapper.insert(refreshToken);
        return refreshToken;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public AuthLoginResp refreshAccessToken(String refreshToken, String clientId) {
        // 查询访问令牌
        SysOAuth2RefreshToken refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken);
        if (refreshTokenDO == null) {
            throw new CustomException("无效的刷新令牌");
        }

        // 校验 Client 匹配
        SystemOath2Client clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
        if (cn.hutool.core.util.ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) {
            throw new CustomException("刷新令牌的客户端编号不正确");
        }

        // 移除相关的访问令牌
        List<SysOAuth2AccessToken> accessTokenDOs = oAuth2AccessTokenMapper.selectListByRefreshToken(refreshToken);
        if (CollUtil.isNotEmpty(accessTokenDOs)) {
            oAuth2AccessTokenMapper.deleteBatchIds(CollUtil.convertSet(accessTokenDOs, SysOAuth2AccessToken::getId));
            List<String> list = CollUtil.convertList(accessTokenDOs, SysOAuth2AccessToken::getAccessToken);
            RdsUtil.del(list);
        }

        // 已过期的情况下，删除刷新令牌
        if (DateUtil.isExpired(refreshTokenDO.getExpiresTime())) {
            oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId());
            throw new CustomException("刷新令牌已过期");
        }

        // 创建访问令牌
        return createOAuth2AccessToken(refreshTokenDO, clientDO);
    }

    @Override
    public R<SysOAuth2AccessTokenCheckResp> checkAccessToken(String accessToken) {
        SysOAuth2AccessToken accessTokenDO = getAccessToken(accessToken);
        if (accessTokenDO == null) {
            return R.fail(ResultCode.UN_AUTHORIZED.getCode(), "访问令牌不存在");
        }
        if (DateUtil.isExpired(accessTokenDO.getExpiresTime())) {
            return R.fail(ResultCode.UN_AUTHORIZED.getCode(), "访问令牌已过期");
        }
        SysOAuth2AccessTokenCheckResp accessTokenCheckResp = BeanUtil.copyProperties(accessTokenDO, SysOAuth2AccessTokenCheckResp.class);
        return R.data(accessTokenCheckResp);
    }

    @Override
    public SysOAuth2AccessToken getAccessToken(String accessToken) {
        // 优先从 Redis 中获取
        SysOAuth2AccessToken accessTokenDO = getCacheLogin(accessToken);
        if (accessTokenDO != null) {
            return accessTokenDO;
        }

        // 获取不到，从 MySQL 中获取访问令牌
        accessTokenDO = oAuth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            // 特殊：从 MySQL 中获取刷新令牌。原因：解决部分场景不方便刷新访问令牌场景
            // 例如说，积木报表只允许传递 token，不允许传递 refresh_token，导致无法刷新访问令牌
            // 再例如说，前端 WebSocket 的 token 直接跟在 url 上，无法传递 refresh_token
            SysOAuth2RefreshToken refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(accessToken);
            if (refreshTokenDO != null && !DateUtil.isExpired(refreshTokenDO.getExpiresTime())) {
                accessTokenDO = convertToAccessToken(refreshTokenDO);
            }
        }

        // 如果在 MySQL 存在，则往 Redis 中写入
        if (accessTokenDO != null && !DateUtil.isExpired(accessTokenDO.getExpiresTime())) {
            setCacheLogin(accessTokenDO);
        }
        return accessTokenDO;
    }

    private SysOAuth2AccessToken convertToAccessToken(SysOAuth2RefreshToken refreshTokenDO) {
        SysOAuth2AccessToken accessTokenDO = BeanUtil.toBean(refreshTokenDO, SysOAuth2AccessToken.class)
                .setAccessToken(refreshTokenDO.getRefreshToken());
        return accessTokenDO;
    }

    private SysOAuth2AccessToken getCacheLogin(String accessToken) {
        String redisKey = formatKey(accessToken);
        return JSON.parseObject(RdsUtil.get(redisKey), SysOAuth2AccessToken.class);
    }

    /**
     * 加载用户信息，方便获取到昵称、部门等信息
     *
     * @param userId 用户编号
     * @param userType 用户类型
     * @return 用户信息
     */
    private LoginUser buildUserInfo(Long userId, Integer userType) {
        R<LoginUser> r = foundationUserApi.getLoginUserById(userId);
        // 用户是否存在
        if (!r.isSuccess() || ObjectUtil.isNull(r.getData())) {
            log.info("create oauth access token build user info: USER_NOT_EXIST");
            throw new CustomException(ModuleErrorCodeConstants.USER_NOT_EXISTS.getMsg());
        }
        return r.getData();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public SysOAuth2AccessToken removeAccessToken(String accessToken) {
        // 删除访问令牌
        SysOAuth2AccessToken accessTokenDO = oAuth2AccessTokenMapper.selectByAccessToken(accessToken);
        if (accessTokenDO == null) {
            return null;
        }
        oAuth2AccessTokenMapper.del(accessTokenDO.getId());
        String key = formatKey(accessToken);
        RdsUtil.del(key);
        // 删除刷新令牌
        oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken());
        return accessTokenDO;
    }

}
